using System;
using System.Collections.Generic;
using System.Data;
using System.Linq;
using Antlr.StringTemplate;

namespace Mite
{
	public abstract class SqlQueryFactory : ISqlQueryFactory
	{
		protected SqlQueryFactory(string templateDirectory)
		{
			TemplateDirectory = templateDirectory;
		}

		protected string TemplateDirectory { get; private set; }

		// TODO: in the remarks section of xml comments on each method, document template variables passed to the template by that method.

		#region ISqlQueryFactory Members

		public string CreateTable(Table table)
		{
			Guard.ArgumentNotNull("table", table);

			const string templateName = "CreateTable";

			StringTemplate template = GetStringTemplateGroup().GetInstanceOf(templateName);
			template.SetAttribute("name", table.Name);

			foreach (Column column in table.Columns)
				template.SetAttribute("columns", ConvertColumn(column));

			return template.ToString();
		}

		public string DropTable(Table table)
		{
			Guard.ArgumentNotNull("table", table);

			const string templateName = "DropTable";

			StringTemplate template = GetTemplate(templateName);
			template.SetAttribute("name", table.Name);

			return template.ToString();
		}

		public string RenameTable(Table table, string newName)
		{
			Guard.ArgumentNotNull("table", table);
			Guard.StringArgumentNotNullOrEmpty("newName", newName);

			const string templateName = "RenameTable";

			StringTemplate template = GetTemplate(templateName);
			template.SetAttribute("oldName", table.Name);
			template.SetAttribute("newName", newName);

			return template.ToString();
		}

		public string RenameColumn(Column column, string newName)
		{
			Guard.ArgumentNotNull("column", column);
			Guard.StringArgumentNotNullOrEmpty("newName", newName);

			if (column.Table == null)
				throw new InvalidOperationException(string.Format("Column is not associated with a table: {0}", column.Name));

			const string templateName = "RenameColumn";

			StringTemplate template = GetTemplate(templateName);
			template.SetAttribute("table", column.Table.Name);
			template.SetAttribute("oldName", column.Name);
			template.SetAttribute("newName", newName);

			return template.ToString();
		}

		public string AlterColumn(Column column, Column newColumn)
		{
			Guard.ArgumentNotNull("column", column);
			Guard.ArgumentNotNull("newColumn", newColumn);

			if (!column.Name.Equals(newColumn.Name))
				throw new InvalidOperationException("Column names must match.");

			if (column.Table !=
			    newColumn.Table)
				throw new InvalidOperationException("column and newColumn must belong to the same table.");

			const string templateName = "AlterColumn";

			StringTemplate template = GetTemplate(templateName);
			template.SetAttribute("column", ConvertColumn(newColumn));
			template.SetAttribute("table", column.Table);

			return template.ToString();
		}

		public string CreateIndex(Index index)
		{
			const string templateName = "CreateIndex";

			StringTemplate template = GetStringTemplateGroup().GetInstanceOf(templateName);
			template.SetAttribute("name", index.Name);
			template.SetAttribute("table", index.Table.Name);
			template.SetAttribute("unique", index.IsUnique);

			foreach (object key in index.Options)
				template.SetAttribute(key.ToString(), index.Options[key]);

			foreach (Column column in index.Columns)
				template.SetAttribute("columns", column.Name);

			return template.ToString();
		}

		public string DropIndex(Index index)
		{
			const string templateName = "DropIndex";

			StringTemplate template = GetStringTemplateGroup().GetInstanceOf(templateName);
			template.SetAttribute("index", index.Name);
			template.SetAttribute("table", index.Table.Name);

			return template.ToString();
		}

		public string AddColumnToTable(Table table, Column column)
		{
			Guard.ArgumentNotNull("table", table);
			Guard.ArgumentNotNull("column", column);

			const string templateName = "AddColumnToTable";

			StringTemplate template = GetTemplate(templateName);
			template.SetAttribute("table", table.Name);
			template.SetAttribute("column", ConvertColumn(column));

			return template.ToString();
		}

		/// <summary>
		/// Generate sql to drop <paramref name="columns"/>.
		/// Columns must all be associated with the same table.
		/// </summary>
		/// <remarks>
		/// Template Variables:
		/// <list>
		/// <item>table: Table with which the columns are associated.</item>
		/// <item>columns: Contains an entry for each column to be dropped.</item>
		/// </list>
		/// </remarks>
		/// <param name="table">Table to which the columns belong.</param>
		/// <param name="columns">Columns to drop.</param>
		public string DropColumns(string table, params string[] columns)
		{
			Guard.ArgumentNotNull("column", columns);
			Guard.SequenceNotNullOrEmpty("columns", columns);

			const string templateName = "DropColumn";

			StringTemplate template = GetTemplate(templateName);
			template.SetAttribute("table", table);
			foreach (string column in columns)
				template.SetAttribute("columns", column);

			return template.ToString();
		}

		/// <summary>
		/// Generate sql to add a foreign key.
		/// </summary>
		/// <param name="table">Table for the constraint.</param>
		/// <param name="referenceTable">Reference table for the constraint.</param>
		/// <param name="name">Name of the foreign key constraint.</param>
		/// <param name="referenceMap">Key value pairs associating a column in the table to a column in the reference table.</param>
		public string AddForeignKey(string table, string referenceTable, string name, params KeyValuePair<string, string>[] referenceMap)
		{
			Guard.StringArgumentNotNullOrEmpty("table", table);
			Guard.StringArgumentNotNullOrEmpty("referenceTable", referenceTable);
			Guard.SequenceNotNullOrEmpty("referenceMap", referenceMap);

			StringTemplate template = GetTemplate("AddForeignKey");
			template.SetAttribute("table", table);
			template.SetAttribute("referenceTable", referenceTable);
			template.SetAttribute("constraintName", name);

			foreach (KeyValuePair<string, string> refMap in referenceMap)
			{
				template.SetAttribute("column", refMap.Key);
				template.SetAttribute("referenceColumn", refMap.Value);
			}

			return template.ToString();
		}

		/// <summary>
		/// Generate sql to drop foreign key constraint.
		/// </summary>
		/// <remarks>
		/// Template Variables:
		/// <list>
		/// <item>
		/// table: Table that the constraint belongs to.
		/// constraint: Name of the constraint to drop.
		/// </item>
		/// </list>
		/// </remarks>
		public string DropConstraint(string table, string name)
		{
			Guard.ArgumentNotNull("key", name);

			StringTemplate template = GetTemplate("DropConstraint");
			template.SetAttribute("constraint", name);
			template.SetAttribute("table", table);

			return template.ToString();
		}

		#endregion

		/// <summary>
		/// Convert <paramref name="dbType"/> into an appropriate database-specific type string.
		/// </summary>
		protected abstract string DbTypeToString(DbType dbType);

		/// <summary>
		/// Transform <paramref name="column"/> into an object to be passed to string templates.
		/// </summary>
		protected virtual object ConvertColumn(Column column)
		{
			object referenceTable = null;
			object referenceColumn = null;

			if (column.References != null)
			{
				referenceTable = column.References.Table.Name;
				referenceColumn = column.References.Name;
			}

			return new
			       {
			       	column.Name,
			       	DataType = DbTypeToString(column.DataType),
			       	column.Length,
			       	column.Precision,
			       	column.Scale,
			       	Identity = column.IsIdentity,
			       	Unique = column.IsUnique,
			       	PrimaryKey = column.IsPrimaryKey,
			       	Nullable = column.IsNullable,
					ForeignKey = column.IsForeignKey,
					ReferenceTable = referenceTable,
					ReferenceColumn = referenceColumn
			       };
		}

		private StringTemplateGroup GetStringTemplateGroup()
		{
			return new StringTemplateGroup("mssql", Environment.CurrentDirectory + TemplateDirectory);
		}

		private StringTemplate GetTemplate(string templateName)
		{
			return GetStringTemplateGroup().GetInstanceOf(templateName);
		}
	}
}